package com.basker.pisces.core.meta;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import com.basker.pisces.config.ApplicationConfig;
import com.basker.pisces.core.meta.annotation.TimestampField.TimestampType;
import com.basker.pisces.exception.ExceptionFactory;
import com.basker.pisces.spring.SpringApplicationContext;

/**
 * 定义了基础的对象元信息的数据结构,可以作为对象元信息实现类的基类
 * <p>
 * 内部定义了若干存放各种属性和{@link IField}的字段，通过{@link #addField(IField)}方法添加各种类型的字段
 *
 * @author hangwen
 */
public abstract class AbstractDataObjectMeta implements IDataObjectMeta {

    private String name;
    private String displayName;
    private String tableName;
    private String schema;
    private String[] subTableNames;
    private IDataField pkField;
    private IDataField codeField;
    private IDataField nameField;
    private IDataField timestampField;
    private TimestampType timestampType;
    private boolean readOnly = false;
    private Map<String, IField> fieldMap = new LinkedHashMap<String, IField>();
    private List<IDataField> dataFields = new ArrayList<IDataField>();
    private List<IDataField> indexFields = new ArrayList<IDataField>();
    private List<IDataObjectField> dataObjectFields = new ArrayList<IDataObjectField>();
    private List<IReferenceField> refrenceFields = new ArrayList<IReferenceField>();
    private List<ICollectionField> collectionFields = new ArrayList<ICollectionField>();
    private AtomicInteger fieldOrder = new AtomicInteger(0);

    public IField addField(IField field) {
        if (field instanceof IReferenceField) {
            return this.addField((IReferenceField) field);
        } else if (field instanceof IDataObjectField) {
            return this.addField((IDataObjectField) field);
        } else if (field instanceof IDataField) {
            return this.addField((IDataField) field);
        } else if (field instanceof ICollectionField) {
            return this.addField((ICollectionField) field);
        }

        return null;
    }

    @Override
    public boolean containsField(String name) {
        return fieldMap.containsKey(name);
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }

        if (!obj.getClass().equals(this.getClass())) {
            return false;
        }

        return Objects.equals(this.name, ((IDataObjectMeta) obj).getName());
    }

    @Override
    public IDataField getCodeField() {
        return codeField;
    }

    @Override
    public Collection<ICollectionField> getCollectionFields() {
        return collectionFields;
    }

    @Override
    public Collection<IDataField> getDataFields() {
        return dataFields;
    }

    @Override
    public Collection<IDataObjectField> getDataObjectFields() {
        return dataObjectFields;
    }

    @Override
    public String getDisplayName() {
        return displayName;
    }

    @Override
    public IField getField(String name) {
        return fieldMap.get(name);
    }

    @Override
    public Set<String> getFieldNames() {
        return Collections.unmodifiableSet(fieldMap.keySet());
    }

    @Override
    public Collection<IField> getFields() {
        return fieldMap.values();
    }

    @Override
    public Collection<IDataField> getIndexFields() {
        return indexFields;
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public IDataField getNameField() {
        return nameField;
    }

    @Override
    public IDataField getPkField() {
        return pkField;
    }

    @Override
    public Collection<IReferenceField> getReferenceFields() {
        return refrenceFields;
    }

    @Override
    public String getSchema() {
        return schema;
    }

    @Override
    public String[] getSubTableNames() {
        return this.subTableNames;
    }

    @Override
    public String getTableName() {
        return this.tableName;
    }

    @Override
    public IDataField getTimestampField() {
        return timestampField;
    }

    @Override
    public TimestampType getTimestampType() {
        return timestampType;
    }

    @Override
    public int hashCode() {
        return StringUtils.isEmpty(this.name) ? super.hashCode() : this.name.hashCode();
    }

    @Override
    public boolean isReadOnly() {
        return readOnly;
    }

    public void setCodeField(IDataField codeField) {
        this.codeField = codeField;
    }

    public void setDisplayName(String displayName) {
        this.displayName = displayName;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setNameField(IDataField nameField) {
        this.nameField = nameField;
    }

    public void setReadOnly(boolean readOnly) {
        this.readOnly = readOnly;
    }

    public void setSchema(String schema) {
        this.schema = this.resolveSchema(schema);
    }

    public void setSubTableNames(String[] subTableNames) {
        this.subTableNames = subTableNames;
    }

    public void setTableName(String tableName) {
        this.tableName = tableName;
    }

    public void setTimestampField(IDataField timestampField) {
        this.timestampField = timestampField;
    }

    public void setTimestampType(TimestampType timestampType) {
        this.timestampType = timestampType;
    }

    @Override
    public String toString() {
        return this.getClass().getSimpleName() + ":" + this.name;
    }

    protected IField internalAddField(IField field) {
        String fieldName = field.getName();
        if (this.fieldMap.containsKey(fieldName)) {
            throw ExceptionFactory.createRuntimeException("pisces-core.AbstractDataObjectMeta.internalAddField-001",
                    this.name, fieldName);
        }

        field.setOrder(fieldOrder.getAndIncrement());
        this.fieldMap.put(fieldName, field);

        return field;
    }

    protected final void valid() {
        this.validPkField();
        this.validCodeNameField();
        this.validateTimestampField();
        this.validateDataFieldPrimitive();
    }

    private IField addField(ICollectionField field) {
        collectionFields.add(field);
        return this.internalAddField(field);
    }

    private IField addField(IDataField field) {
        dataFields.add(field);

        if (field.isPkField()) {
            pkField = field;
        } else if (field.isIndexField()) {
            indexFields.add(field);
        }

        String hostTableName = field.getHostTableName();
        if (StringUtils.isEmpty(hostTableName)) {
            field.setHostTableName(this.tableName);
        }

        return this.internalAddField(field);
    }

    private IField addField(IDataObjectField field) {
        dataObjectFields.add(field);
        return this.internalAddField(field);
    }

    private IField addField(IReferenceField field) {
        refrenceFields.add(field);

        this.addField((IDataObjectField) field);
        this.addField(field.getReferenceIdField());

        return field;
    }

    private String resolveSchema(String schema) {
        if (StringUtils.isEmpty(schema)) {
            return null;
        }

        ApplicationConfig config = SpringApplicationContext.getConfig();
        String resolvedSchema = config != null ? config.resolvePlaceholders(schema) : schema;

        if (resolvedSchema.equals(schema)) {
            // 没有找到配置，则返回null，这样当前元数据的schema就是默认数据库
            return null;
        }

        return resolvedSchema;
    }

    private void validateDataFieldPrimitive() {
        for (IDataField dataField : this.dataFields) {
            if (dataField.getFieldClass().isPrimitive()) {
                throw new RuntimeException(
                        "DataObjectMeta '" + this.name + "' field '" + dataField.getName() + "' is primitive type!");
            }
        }
    }

    private void validateTimestampField() {
        if (this.timestampField == null) {
            return;
        }

        Assert.isTrue(timestampField.getFieldClass().equals(Long.class), "timestampField must be Long Type!");
        Assert.isTrue(timestampField.getHostTableName().equalsIgnoreCase(this.tableName),
                "timestampField's host table must be main table!");
    }

    private void validCodeNameField() {
        if (this.codeField != null) {
            Assert.isTrue(codeField.getFieldClass().equals(String.class), "codeField must be String Type!");
        }

        if (this.nameField != null) {
            Assert.isTrue(nameField.getFieldClass().equals(String.class), "nameField must be String Type!");
        }
    }

    private void validPkField() {
        boolean hasTableName = !StringUtils.isEmpty(this.getTableName());

        if (hasTableName && this.pkField == null) {
            throw new RuntimeException("DataObjectMeta '" + this.name + "' has no pkField!");
        }
    }

}
